# Практика код ## Вычисление суммы без использования буфера Пример, где подсчитывается сумма по записям коллекции Oil_PlanFactoryShipPos: ```scala //получаем все записи коллекции по родителю (объект rop) Oil_PlanFactoryShipPosApi().byParent(rop) //получаем только значение поля nQtyLoad //в случае незаполненности поля необходимо иметь значение 0, иначе в дальнейшем сумма с null = null .map(_.get(_.nQtyLoad).nvl(0.nn)) //сложение, результат которого будет обернут безопасной конструкцией Option .reduceOption(_ + _) //распаковка Option //если внутри был null или в коллекции не было записей, то вернётся указанное значение 0.nn .getOrElse(0.nn) ``` ## Группировка объектов с использованием null-типов При группировке объектов коллекций, в которых используется null-типы нужно учитывать, что метод `groupBy` считает хэши группируемых объектов, поэтому перед группировкой, нужно убедиться, что параметр, используемый для группировки, не равен null. Пример, с NPE: ```scala List( asd(123.nl, "asd".ns), asd(None.nl, "asd".ns), // У NLong с underlying = null хэш не посчитается ).groupBy(_.id).foreach { case (_, name) => println(name) } ``` Перед использованием `groupBy` необходимо отфильтровать значения или использовать метод `nvl`. ## Сравнение диапазона дат Допустим документы имеют поля dBeginDoc и dEndDoc. Фильтр имеет 2 поля dBeginFilter и dEndFilter. Чтобы найти все документы, которые началом или окончанием входят в диапазон, указанный в фильтре, нужно использовать следующее условие: ```scala dBeginDoc <= dEndFilter && dEndDoc >= dBeginFilter ``` ## .distinct или .toSet для scala-коллекций и особенности применения Если необходимо в scala-коллекции держать только уникальные объекты, можно использовать методы: - `.distinct` - `.toSet` Перед использованием метода `.distinct` scala-коллекцию необходимо подготовить: убрать значения null. Иначе в ходе выполнения программы выпадет ошибка `java.lang.NullPointerException`. Пример использования и демонстрация поведения методов: ```scala test("distinctOrToSet") { val data: Seq[NString] = Seq("h".ns, "i".ns, "i".ns, None.ns, None.ns) println("Результат работы .distinct с null внутри scala-коллекции:") try { println(data.distinct) } catch { case e: Throwable => println("Ошибка:\n" + "java.lang.NullPointerException") } finally { println("\nРезультат работы .filter(_.isNotNull).distinct:") println(data.filter(_.isNotNull).distinct) try { println("\nРезультат работы .toSet с null внутри scala-коллекции:") println(data.toSet) } catch { case e: Throwable => println("Ошибка:\n" + "java.lang.NullPointerException\n") } } } ``` Результат: ```{note} Результат работы .distinct с null внутри scala-коллекции: Ошибка: java.lang.NullPointerException Результат работы .filter(_.isNotNull).distinct: List(h, i) Результат работы .toSet с null внутри scala-коллекции: Set(h, i, Null) ``` ## `immutable.Map.builder` вместо `mutable.Map` Если по результату сформированной Map она больше не изменяется, то для формирования лучше использовать конструктор `immutable.Map.builder`, чем `mutable.Map`. Пример: ```scala val map = Map.newBuilder[NString, NString] map ++= Map("1" -> "11") map += "2" -> "22" map.result() //Map("1" -> "11", "2" -> "22") ``` ## Признак наличия модуля на проекте ```scala val isInstallProModule = session.sbtClassLoader.getModuleMap.containsKey("pro") ``` Вернёт true, если такой модуль есть в проекте, иначе false. ```{warning} Поддержка этого метода не гарантируется. ``` ## Применение ASQL/ ASelect/ OQuery/ TxIndex/ refreshByParent и byParent Про данные инструменты можно почитать [здесь](../../020_common/055_взаимодействие_с_базой.md#взаимодействие-с-базой-данных). ### ASQL Выполнение реляционного запроса на чтение. [ASQL](../../020_common/055_взаимодействие_с_базой.md#asql) ```{attention} Вызывает транзакцию в БД, не учитывая значения в кэше. ``` Удобен для получения значения по одному столбцу результата запроса. ```scala val idaWagonByTrain = ASQL""" select string_agg(cast(rwt.idWagon as varchar), ', ') from Rzd_TrainWagon rwt where rwt.idTrain = $idpTask """ //берется 1ый столбец результата запроса, как NString //если результат запроса вернул несколько строк, то будет взята первая в конструкции Option, // но предполагается, что результат вернёт максимум 1 строку .as(nStr(1).singleOpt) //если Option пустой, т.е. в результате запроса не было строк, то вернётся None.ns .getOrElse(None.ns) ``` `$idpTask` это подстановка значения из переменной scala `idpTask` в запрос связанной переменной (binding). ```{attention} Нельзя использовать внутри цикла. Это приведёт к многочилсенным транзакциям в БД. ``` Вместо использования `ASQL` внутри цикла с множественными транзакциями в БД нужно перед циклом одной транзакцией сформировать массив данных, который дальше будет использоваться внутри цикла. ### ATSQL Выполнение реляционного запроса с изменением данных или блокировками. [ATSQL](../../020_common/055_взаимодействие_с_базой.md#atsql) Используется редко, в основном в ядровых процедурах, потому что минует серверную логику, записи в системные миксины, ведение аудитов и другое. ### ASelect Выполнение реляционного запроса на чтение/запись. [ASelect](../../020_common/055_взаимодействие_с_базой.md#aselect) ```{attention} Вызывает транзакцию в БД, не учитывая значения в кэше. ``` Используется в основном для чтения, потому что при изменении данных минует серверную логику, записи в системные миксины, ведение аудитов и другое. Удобен для получения значений по нескольким столбцам результата запроса. ```scala val mapPerson: Map[NLong, NString] = new ASelect { val sCode = asNString("sCode") val id = asNLong("id") SQL""" select p.id ,p.sCode from Bs_Person p where idObjectType = $idvObjectType """ }.map { rv => //rv представляет собой одну строку результата запроса rv.id() -> rv.sCode() }.toMap ``` `$idvObjectType` это подстановка значения из переменной scala `idvObjectType` в запрос связанной переменной (binding). ```{attention} Нельзя использовать внутри цикла. Это приведёт к многочилсенным транзакциям в БД. ``` Вместо использования `ASelect` внутри цикла с множественными транзакциями в БД нужно перед циклом одной транзакцией сформировать массив данных, который дальше будет использоваться внутри цикла. ### Когда использовать ASQL, а когда ASelect ASQL удобен для получения значения по одному столбцу результата запроса. ASelect удобен для получения значений по нескольким столбцам результата запроса. ### Подстановка связанных переменных (binding) Актуально для инструментов ASQL, ATSQL, ASelect. ```{note} Использование ASQL с подстановкой связанных переменных является полезной практикой, потому что запрос остаётся неизменным, меняется лишь значение параметра. Тем самым запрос не воспринимается системой, как новый, и будет записан в системную таблицу запросов единожды, что не приводит к распуханию БД. ``` Инструмент `ASQL"""<текст запроса>"""` подставляет бинды с учётом типа данных переменной. - Если бы была подстановка `NString`, то `ASQL` сам обернул бы значение в одинарные кавычки, т.е. нет необходимости их указывать вручную. - Если `NString` подставляется в текст `s"""<текст запроса>"""`, то одинарные кавычки необходимо указывать вручную. Если текст запроса для `ASQL"""<текст запроса>"""` собирается динамически средствами scala, в том числе название таблицы для select формируется переменной, то его необходимо подставлять через ```#$bind```, чтобы ASQL не обернул подставляемое значение в одинарные кавычки. ```scala val sNameTb = { if (a = 1) "Bs_Goods".ns else "Bs_Person".ns } val ida: List[NLong] = ASQL""" select t.id from #$sNameTb where idObjectType = $idvObjectType """.as(nLong(1).*) ``` Существует иной формат binding, когда запрос формируется в переменной String/NString, где указываются ключи для подстановки значения, карта подстановки указывается в ```.on()``` ```scala val svRequest = s""" select t.id from Bs_Goods where idObjectType = {idObjectType} """ ASQL(svRequest).on("idObjectType" -> idvObjectType) new ASelect { val sCode = asNString("sCode") val id = asNLong("id") SQL(s""" select p.id ,p.sCode from Bs_Person p where idObjectType = {idObjectType} """) .on("idObjectType" -> idvObjectType) }.map { rv => //rv представляет собой одну строку результата запроса rv.id() -> rv.sCode() }.toMap ``` Может быть полезно, если запрос собирается в переменной String/NString. ### OQuery - Объектный запрос - Синтаксис схож с реляционным запросом - Возвращает результат с учётом кэша - Результат - список Rop, что удобно для использования методов, которым требуется Rop [OQuery](../../020_common/055_взаимодействие_с_базой.md#объектные-запросы) Пример запроса: ```scala new OQuery(Bs_GoodsAta.Type) { where(t.sSystemName === spMnemoCode) }.toVector ``` Особенности, которые нужно знать про OQuery: #### Условие применяется для запроса в БД Условие применяется для запроса в БД. Поэтому поиск записей идёт в БД без учёта кэша, а результат возвращается с учётом кэша. Пример: ```scala val idvGds = 121.nl val ropGds = Bs_GoodsApi().load(idvGds) //ТМЦ переводится в состояние Отменено (0) Bs_GoodsApi().setidState(ropGds, idvStateCancel) //Поиск неотмененных ТМЦ с условием по категории new OQuery(Bs_GoodsAta.Type) { where(t.idCategory = idvGdsCategory and t.idStateMC >> 0.nn) }.toVector ``` В результате OQuery будет запись Bs_Goods c id = 121, при чём с состоянием "Отменено" , потому что: - в БД эта запись не Отменена (t.idStateMC > 0) , а изменение по отмене записи находится в кэше и не синхронизировано с БД - результат OQuery возвращается с учётом кэша , поэтому на запись c id = 121 применён кэш - смена состояния Решить можно 2 способами: ```scala /** Вариант 1 - синхронизация с БД */ //синхронизация с БД session.flush() //Поиск неотмененных ТМЦ с условием по категории new OQuery(Bs_GoodsAta.Type) { where(t.idCategory = idvGdsCategory and t.idStateMC >> 0.nn) }.toVector /** Вариант 2 - вынос условия из OQuery, фильтрация результата OQuery */ //Поиск неотмененных ТМЦ с условием по категории new OQuery(Bs_GoodsAta.Type) { where(t.idCategory = idvGdsCategory) } //вынос условия из OQuery .filter(_.get(_.idStateMC) > 0.nn) ``` ```{warning} 1. Вариант 1 - синхронизация с БД с помощью session.flush() имеет недостаток: у пользователя пропадает возможность отменить изменения на выборке. 2. Вариант 2 - вынос условия из OQuery, фильтрация результата OQuery имеет недостаток: OQuery в память выгружает больше записей из БД, что может требовать приемов по работе с большими данными (см. раздел "Разработка под высокую нагрузку"). ``` #### Для предварительной прогрузки записей в кэш необходима инициализация OQuery Про предварительную прогрузку записей в кэш можно прочитать [здесь](../optimization/cache.md#используйте-предварительную-прогрузку-данных-в-кэш-через-batchin-и-querykeys). Чтобы данные были загружены в кэш, необходима инициализация OQuery , которая происходит при использовании обходчика ```foreach``` или преобразовании OQuery к иной коллекции: ```scala //Предварительная выгрузка записей в кэш new OQuery(Bs_GoodsAta.Type) { where(t.sSystemName === spMnemoCode and t.idStateMC >> 0.nn) } //для инициализации OQuery .toVector ``` ### Транзакционный индекс - Объектный запрос - Поиск по одному полю по условию равенства - Нет возможности искать по составному индексу из нескольких полей класса - Ищет и возвращает результат с учётом кэша - Результат - список Rop, что удобно для использования методов, которым требуется Rop [Транзакционный индекс](../../020_common/055_взаимодействие_с_базой.md#транзакционный-индекс). Пример запроса: ```scala //Объявляется обычно в Api класса соответствующего Ata (у примера Bs_GoodsApi) lazy val idxCategory = TxIndex(Bs_GoodsAta.Type)(_.idCategory) /** Поиск неотмененных ТМЦ по категории */ def byCategoryNotCanceled(): Iterable[ApiRop] = { idxCategory.byKey(idvGdsCategory) .filter(_.get(_.idStateMC) > 0.nn) } ``` Если нужен запрос по нескольким условиям, то используйте индекс по полю, который вернёт меньшее кол-во записей в память, после чего примените остальные условия используя ```.filter()```. Если транзакционный индекс используется в цикле, то необходимо предварительно загрузить результаты в память по множеству ключей. Об этом можно почитать [здесь](../optimization/cache.md#используйте-предварительную-прогрузку-данных-в-кэш-через-batchin-и-querykeys). ### refreshByParent и byParent - Объектный запрос - Имеется только у коллекций - Поиск по ссылочному полю на мастера - Возвращает результат с учётом кэша - Результат - список Rop, что удобно для использования методов, которым требуется Rop - ```refreshByParent``` инвалидирует записи из БД и возвращает актуальные данные, гарантированно делает транзакцию в БД. - ```byParent``` возвращает данные из памяти, если они там были, иначе первично получает актуальные данные из БД, делая транзакцию. - Перед использованием ```byParent``` в цикле для предварительной прогрузки в кэш можно использовать ```OQuery``` ([предварительная прогрузка](../optimization/cache.md#используйте-предварительную-прогрузку-данных-в-кэш-через-batchin-и-querykeys).) Про разницу между ```refreshByParent``` и ```byParent``` можно почитать [здесь](../optimization/cache.md#применяйте-правильно-byparent-и-bykey-вместо-refreshbyparent-и-refreshbykey). ### Какой инструмент использовать [Загрузка множества объектов](../optimization/cache.md#загрузка-множества-объектов-) Если нужны записи с учётом кэша, то используйте: - объектный запрос - реляционный запрос с предварительным session.flush (не для построения выборок) ```{warning} 1. Синхронизация с БД с помощью session.flush() имеет недостаток: у пользователя пропадает возможность отменить изменения на выборке. 2. Для построения выборок, в которых ведутся изменения объекта, используйте объектный запрос. ```